/**
 * 
 */
package jmine.tec.report.impl;

import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import jmine.tec.report.ExecutableDataRow;
import jmine.tec.report.metadata.ColumnMetaData;
import jmine.tec.report.metadata.ParameterDefinition;
import jmine.tec.report.metadata.ParameterType;
import jmine.tec.report.metadata.impl.UnmodifiableColumnMetaData;
import jmine.tec.utils.Tuple;
import jmine.tec.utils.UnmodifiableTuple;
import jmine.tec.utils.date.DatePeriod;
import jmine.tec.utils.date.PastDatePeriod;

/**
 * Superclass to executable data columns that uses a period of time. Subclasses are encouraged to call
 * {@link #findDatePeriod(Map, Collection)} to fetch the dates from the parameter {@link Map}.
 * 
 * @author takeshi
 * @version 24/08/2007
 */
public class SimplePeriodicDataColumn extends AbstractExecutableDataColumn {

    private static final long serialVersionUID = -4622890442210248013L;

    public static final String END_DATE_LABEL = "maps.report.column.parameter.enddate";

    public static final String END_DATE_PARAM_KEY = "ExecutableDataColumn#params#endDate";

    public static final String START_DATE_LABEL = "maps.report.column.parameter.startdate";

    public static final String START_DATE_PARAM_KEY = "ExecutableDataColumn#params#startDate";

    /**
     * Merges parameter definitions
     * 
     * @param key the column key
     * @param name the column name
     * @param meta the other {@link ColumnMetaData}
     * @param params the params
     * @return {@link ColumnMetaData} merged
     */
    protected static ColumnMetaData mergeMetaData(final String key, final String name, final ColumnMetaData meta,
            final ParameterDefinition... params) {
        Map<String, ParameterDefinition> def = new HashMap<String, ParameterDefinition>();
        if (meta != null) {
            def.putAll(meta.getAllParameterDefinitions());
        }
        def.put(START_DATE_PARAM_KEY, getStartDateParameter());
        def.put(END_DATE_PARAM_KEY, getEndDateParamter());
        return new UnmodifiableColumnMetaData(key, name, def.values(), params);
    }

    /**
     * C'tor
     * 
     * @param key the key
     * @param name the name
     * @param metaData the metadata
     * @param otherParams the other params
     */
    public SimplePeriodicDataColumn(final String key, final String name, final ColumnMetaData metaData,
            final ParameterDefinition... otherParams) {
        super(key, name, mergeMetaData(key, name, metaData, otherParams));
    }

    /**
     * C'tor
     * 
     * @param key the column key
     * @param name the column name
     * @param params the params
     */
    public SimplePeriodicDataColumn(final String key, final String name, final ParameterDefinition... params) {
        super(key, name, mergeMetaData(key, name, null, params));
    }

    /**
     * Finds a date period with a reference date, inverting the order if the invert parameter is <code>true</code>. Will throw
     * {@link IllegalArgumentException} if the "end" parameter is not fof types: {@link Date}, {@link Calendar}, {@link DatePeriod} or
     * {@link PastDatePeriod}.
     * 
     * @param reference the reference {@link Date}
     * @param end the end
     * @param invert if true, the end will be inverted
     * @param holidays the {@link Collection} with holidays
     * @return {@link Tuple}
     */
    private static Tuple<Date, Date> findDatePeriod(final Date reference, final Object end, final boolean invert,
            final Collection<? extends Date> holidays) {
        if (end instanceof Date) {
            return (UnmodifiableTuple.newTuple(reference, (Date) end));
        } else if (end instanceof Calendar) {
            return (UnmodifiableTuple.newTuple(reference, ((Calendar) end).getTime()));
        } else if (end instanceof DatePeriod) {
            DatePeriod dp = (DatePeriod) end;
            if (invert) {
                dp = dp.invert();
            }
            Date bound = dp.plusDate(reference, holidays);
            return UnmodifiableTuple.newTuple(reference, bound);
        } else if (end instanceof PastDatePeriod) {
            return ((PastDatePeriod) end).getBounds(reference);
        }
        throw new IllegalArgumentException(end + " is not of required type: " + end.getClass());
    }

    /**
     * Method to find the holidays of a given row. Subclasses are allowed to change this method for proper behaviour.
     * 
     * @param row the {@link ExecutableDataRow}
     * @return {@link Collections#emptyList()}
     */
    protected Collection<? extends Date> findHolidays(final ExecutableDataRow row) {
        return Collections.emptyList();
    }

    /**
     * Finds a period bounded, that is, with the paramters set: {@link #START_DATE_PARAM_KEY} and {@value #END_DATE_PARAM_KEY}.
     * 
     * @param toUse the map of parameters
     * @param holidays the {@link Collection} with holidays
     * @return {@link Tuple}
     */
    private static Tuple<Date, Date> getDatePeriodWithBounds(final Map<String, Object> toUse,
            final Collection<? extends Date> holidays) {
        Object start = toUse.get(START_DATE_PARAM_KEY);
        Object end = toUse.get(END_DATE_PARAM_KEY);
        if (start instanceof Date) {
            return findDatePeriod((Date) start, end, false, holidays);
        } else if (start instanceof Calendar) {
            return findDatePeriod(((Calendar) start).getTime(), end, false, holidays);
        }

        if (end instanceof Date) {
            return findDatePeriod((Date) end, start, true, holidays);
        } else if (end instanceof Calendar) {
            return findDatePeriod(((Calendar) end).getTime(), start, true, holidays);
        }
        throw new IllegalArgumentException("Neither start nor end dates are of proper type: " + start.getClass()
                + " and: " + end.getClass());
    }

    /**
     * Tries to parse a pair of {@link Date} in the presence of a start date at least.
     * 
     * @param toUse the parameters {@link Map}
     * @param holidays the {@link Collection} with holidays
     * @return a {@link Tuple} of {@link Date}'s
     */
    private static Tuple<Date, Date> getDatePeriodWithStart(final Map<String, Object> toUse,
            final Collection<? extends Date> holidays) {
        if (toUse.containsKey(END_DATE_PARAM_KEY)) {
            return getDatePeriodWithBounds(toUse, holidays);
        }
        Object start = toUse.get(START_DATE_PARAM_KEY);
        if (start instanceof PastDatePeriod) {
            return ((PastDatePeriod) start).getBoundsFromNow();
        }
        throw new IllegalArgumentException("Missing parameter: " + END_DATE_PARAM_KEY
                + " or start parameter not of type " + PastDatePeriod.class);
    }

    /**
     * Makes sure that the returned {@link Tuple#getHead()} is a {@link Date} that is before {@link Tuple#getTail()}. If the argument is
     * <code>null</code>, a <code>null</code> is returned.
     * 
     * @param tuple the {@link Tuple} to sort.
     * @return {@link Tuple} sorted or null.
     */
    public static Tuple<Date, Date> sortDates(final Tuple<Date, Date> tuple) {
        if (tuple == null) {
            return null;
        }
        if (tuple.getHead().after(tuple.getTail())) {
            return tuple.invert();
        }
        return tuple;
    }

    /**
     * Finds a date period from the map, either as {@link Date}, {@link Calendar}, {@link DatePeriod}, {@link PastDatePeriod}. Returns
     * <code>null</code> if none of those can be found.
     * 
     * @param params {@link Map}
     * @param holidays the {@link Collection} with holidays
     * @return {@link Tuple} where the {@link Tuple#getHead()} returns the start {@link Date} and {@link Tuple#getTail()} returns the end
     * {@link Date} of the period
     */
    @SuppressWarnings("unchecked")
    public static final Tuple<Date, Date> findDatePeriod(final Map<String, Object> params,
            final Collection<? extends Date> holidays) {
        final Map<String, Object> toUse = params != null ? params : Collections.EMPTY_MAP;
        if (toUse.containsKey(START_DATE_PARAM_KEY)) {
            return sortDates(getDatePeriodWithStart(toUse, holidays));
        }
        Object end = toUse.get(END_DATE_PARAM_KEY);
        if ((end != null) && (end instanceof PastDatePeriod)) {
            return sortDates(((PastDatePeriod) end).getBoundsFromNow());
        }
        return null;
    }

    /**
     * @return {@link ParameterDefinition}
     */
    public static final ParameterDefinition getStartDateParameter() {
        // def.put(START_DATE_PARAM_KEY,
        // def.put(END_DATE_PARAM_KEY, new
        // ParameterDefinition(END_DATE_PARAM_KEY, END_DATE_LABEL,
        // null, ParameterType.DATE_PERIOD));
        return new ParameterDefinition(START_DATE_PARAM_KEY, START_DATE_LABEL, null, ParameterType.DATE_PERIOD);
    }

    /**
     * @return {@link ParameterDefinition}
     */
    public static final ParameterDefinition getEndDateParamter() {
        return new ParameterDefinition(END_DATE_PARAM_KEY, END_DATE_LABEL, null, ParameterType.DATE_PERIOD);
    }

}
